W9. Inheritance, Polymorphism, Object Class

Author

Eugene Zouev, Munir Makhmutov

Published

November 2, 2025

Quiz | Flashcards

1. Summary

1.1 The Cornerstones of Object-Oriented Programming (OOP)

Object-Oriented Programming is built on three fundamental concepts, often called cornerstones, that help manage the complexity of software.

1.1.1 Encapsulation

Encapsulation is the practice of bundling data (attributes) and the methods that operate on that data within a single unit, or class. It hides the internal state of an object from the outside world. Access to the data is restricted to an object’s own methods, which provides a protective barrier. This is the first cornerstone of OOP and is achieved using access modifiers like public and private.

1.1.2 Inheritance

Inheritance is a mechanism where a new class (the subclass or derived class) acquires the properties and behaviors of an existing class (the superclass or base class). It represents an “is a” relationship. For example, a FamilyCar is a Personal car, which in turn is a Car. This promotes code reuse and creates a logical hierarchy. Inheritance is the second cornerstone of OOP.

1.1.3 Polymorphism

Polymorphism, from the Greek for “many forms,” is the ability of an object to take on many forms. It allows a single interface (like a superclass method) to be used for a general class of actions. The specific action is determined by the exact nature of the situation. For example, a draw() method can be called on a list of different shapes, and each shape will correctly draw itself. This is the third cornerstone of OOP.

1.2 Inheritance in Detail
1.2.1 The Concept of Taxonomy

Inheritance is inspired by the real-world concept of taxonomy, or classification. Just as in science we classify a Lion as a type of Animal, in programming we can define a Lion class that inherits from an Animal class. This allows us to define general features (like hasEyes) in Animal and have them automatically apply to Lion, Cat, and Dog, while each specific animal class can add its own unique features (like maneSize for Lion).

1.2.2 “is a” vs. “has a” Relationships

It’s important to distinguish between inheritance and another type of relationship called delegation or aggregation.

  • Inheritance (“is a”): A PersonalCar is a Car. This relationship is implemented using the extends keyword in Java.
  • Delegation (“has a”): A Car has an Engine. This means the Car class would contain an instance variable of the Engine class. All subclasses of Car, like PersonalCar, would also have an engine by extension.
1.2.3 Single vs. Multiple Inheritance

Programming languages differ in how they implement inheritance.

  • Single Inheritance: A class can inherit from only one superclass. This is simpler to understand and more efficient to implement. It avoids certain complexities like the “diamond problem.” Languages like Java, C#, and Scala use single inheritance. Java uses interfaces to achieve some of the benefits of multiple inheritance.
  • Multiple Inheritance: A class can inherit from more than one superclass. This is more powerful but also more complex. For example, a Villa could be considered both a Building (with properties like number of floors) and a Home (with properties like number of bedrooms). Languages like C++, Eiffel, and Python support multiple inheritance.
1.2.4 Inheritance Terminology in Java

In Java, the extends keyword is used to establish an inheritance relationship. When class A extends B, we can use several synonyms to describe the relationship:

  • Class A inherits from class B.
  • Class A is a subclass of class B.
  • Class B is a superclass of class A.
  • Class A is a child of class B.
  • Class B is the parent of class A.

The terms subclass and superclass are the standard terminology chosen for Java. In contrast, languages like C++ often use the terms derived class and base class.

1.2.5 The “Subobject” Notion

When an object of a subclass is created, it internally contains a complete object of its superclass. This is often called a subobject. For example, if class Derived extends Base, creating a new Derived() object also implicitly creates a Base subobject within it. This is how the derived object gains access to the members of its base class.

1.3 Members and Access Control in Inheritance
1.3.1 Access Rules for Class Members

Java provides modifiers to control the visibility and accessibility of class members (fields and methods).

  • private: Members are accessible only within their own class. They are not accessible in subclasses.
  • protected: This modifier is designed specifically for inheritance. Protected members are accessible within their own class, by all subclasses (even in different packages), and by all classes within the same package.
  • Package-private (default): If no modifier is specified, the member is accessible to any class within the same package.
  • public: Members are accessible from any other class, anywhere.
1.3.2 Overriding vs. Hiding

When a subclass defines a member that has the same name as a member in its superclass, one of two things happens:

  • Hiding (for fields): If a subclass defines a field with the same name as a superclass field, the subclass field hides the superclass field. You can still access the hidden field from the superclass using the super keyword (e.g., super.myField).
  • Method Overriding: If a subclass provides a method with the exact same signature (name and parameters) as a method in its superclass, the subclass method overrides the superclass method. This is a key feature of polymorphism. When the method is called on an object of the subclass, the subclass’s version is executed.
1.4 Polymorphism in Detail
1.4.1 Static and Dynamic Types

To understand polymorphism, one must first understand the difference between an object’s static and dynamic type.

  • Static Type: The declared type of a reference variable. This is fixed at compile time and never changes.
  • Dynamic Type: The actual type of the object that the reference variable points to at runtime. This can change through assignment.

Consider the following code:

// Shape is the static type of the 'figure' variable
Shape figure; 

// The dynamic type of 'figure' is now Circle
figure = new Circle(); 

Here, an object of a derived type (Circle) can be assigned to a reference of a base type (Shape). This is called upcasting.

1.4.2 Late Binding and Dynamic Dispatch

Polymorphism is powered by a mechanism called late binding or dynamic dispatch. The main rule is:

The interpretation of a call to a virtual method depends on the dynamic type of the object for which it is called.

In Java, all non-final, non-static, and non-private methods are virtual by default. This means that when you call a method like figure.draw(), the Java Virtual Machine looks at the actual object figure refers to at that moment (its dynamic type) and calls the draw() method belonging to that specific class (Circle, Rectangle, etc.).

Shape[] figures = new Shape;
figures = new Circle();
figures = new Rectangle();

for (Shape fig : figures) {
    // Dynamic dispatch happens here!
    // If fig is a Circle, Circle's draw() is called.
    // If fig is a Rectangle, Rectangle's draw() is called.
    fig.draw(); 
}
1.4.3 The Advantage of Polymorphism

The primary benefit of polymorphism is that it creates extensible and maintainable code. In the shapes example, we can add a new Triangle class that extends Shape without changing the loop that draws all the figures. The libraries of figures and actions become independent of each other, breaking the strong mutual dependency found in procedural solutions (which often rely on switch statements). Polymorphism allows derived types to modify the behavior of the base type.

1.5 The Java Object Class

In Java, there is a special class named Object.

1.5.1 The Ancestor of All Classes

The Object class is the root of the class hierarchy. Every class in Java is a direct or indirect subclass of Object. If a class declaration does not explicitly use the extends keyword, it implicitly inherits from Object.

1.5.2 Common Methods of the Object Class

Because every class inherits from Object, every object in Java has access to the methods defined in the Object class. These methods provide some common, fundamental behaviors.

Method Description
public final Class getClass() Returns the runtime class (Class object) of this object.
public int hashCode() Returns a hash code value (an integer) for the object.
public boolean equals(Object obj) Compares this object to the specified object for equality. The default behavior checks for reference equality (==).
protected Object clone() throws CloneNotSupportedException Creates and returns a copy of this object.
public String toString() Returns a string representation of the object. By default, this is the class name followed by its hash code.
public final void notify() Wakes up a single thread that is waiting on this object’s monitor. Used in multithreading.
public final void notifyAll() Wakes up all threads that are waiting on this object’s monitor. Used in multithreading.
public final void wait(...) Causes the current thread to wait until another thread invokes notify() or notifyAll() for this object. Used in multithreading.
protected void finalize() throws Throwable Called by the garbage collector on an object when garbage collection determines that there are no more references to the object. Deprecated since JDK 9.

2. Definitions

  • Class: A blueprint or template for creating objects. It defines a set of attributes (data) and methods (behavior).
  • Object: An instance of a class.
  • Encapsulation: The bundling of data and methods that operate on the data into a single unit (a class), and hiding the internal state from the outside.
  • Inheritance: A mechanism allowing a new class (subclass) to adopt the properties and methods of an existing class (superclass). It models an “is a” relationship.
  • Polymorphism: The ability of objects of different classes to respond to the same message (method call) in different, class-specific ways.
  • Superclass: A class from which another class inherits. Also known as a base class or parent class.
  • Subclass: A class that inherits from another class. Also known as a derived class or child class.
  • Method Overriding: Providing a specific implementation in a subclass for a method that is already defined in its superclass. The method signatures must be identical.
  • Hiding: When a subclass defines a field with the same name as a field in its superclass, the subclass’s field conceals the superclass’s field.
  • Static Type: The type of a variable as declared in the source code at compile time.
  • Dynamic Type: The actual class of the object that a variable refers to at runtime.
  • Upcasting: The process of treating an object of a subclass as an object of its superclass.
  • Late Binding (Dynamic Dispatch): The process of determining which specific method implementation to run at runtime, based on the dynamic type of the object.
  • Object Class: The root of the class hierarchy in Java. Every class is a descendant of Object.
  • protected: An access modifier that allows access to a member within its own package and by subclasses.
  • super: A keyword used to refer to members (fields, methods, constructors) of the immediate superclass.

3. Examples

3.1. Animal and Child Classes (Lab 1, Example 1)

Create a class which represents Animal class and its basic properties: name, height, weight, color, and basic operations: eat, sleep, makeSound. Also create child classes which represent the exact animals: cow, cat, dog and override properties / methods. Use inheritance for minimizing amount of code.

Click to see the solution
// Animal.java
// The abstract Animal class defines common properties and behaviors for all animals.
abstract class Animal {
    // Basic properties of an animal
    private String name;
    private double height; // in meters
    private double weight; // in kilograms
    private String color;

    // Constructor to initialize an Animal object
    public Animal(String name, double height, double weight, String color) {
        this.name = name;
        this.height = height;
        this.weight = weight;
        this.color = color;
    }

    // Getters for properties
    public String getName() {
        return name;
    }

    public double getHeight() {
        return height;
    }

    public double getWeight() {
        return weight;
    }

    public String getColor() {
        return color;
    }

    // Basic operations (methods) common to all animals
    public void eat() {
        System.out.println(name + " is eating.");
    }

    public void sleep() {
        System.out.println(name + " is sleeping.");
    }

    // Abstract method for making sound, must be implemented by subclasses
    public abstract void makeSound();

    // toString method for easy printing of Animal details
    @Override
    public String toString() {
        return "Name: " + name + ", Height: " + height + "m, Weight: " + weight + "kg, Color: " + color;
    }
}

// Cow.java
// The Cow class extends Animal, inheriting its properties and behaviors.
class Cow extends Animal {
    // Constructor for Cow, calling the superclass constructor
    public Cow(String name, double height, double weight, String color) {
        super(name, height, weight, color);
    }

    // Overriding the makeSound method for a Cow
    @Override
    public void makeSound() {
        System.out.println(getName() + " says Moo!");
    }

    // Cows might have specific eating habits, overriding if needed
    @Override
    public void eat() {
        System.out.println(getName() + " is grazing on grass.");
    }
}

// Cat.java
// The Cat class extends Animal, inheriting its properties and behaviors.
class Cat extends Animal {
    // Constructor for Cat, calling the superclass constructor
    public Cat(String name, double height, double weight, String color) {
        super(name, height, weight, color);
    }

    // Overriding the makeSound method for a Cat
    @Override
    public void makeSound() {
        System.out.println(getName() + " says Meow!");
    }

    // Cats might have specific sleeping habits, overriding if needed
    @Override
    public void sleep() {
        System.out.println(getName() + " is curled up and napping.");
    }
}

// Dog.java
// The Dog class extends Animal, inheriting its properties and behaviors.
class Dog extends Animal {
    // Constructor for Dog, calling the superclass constructor
    public Dog(String name, double height, double weight, String color) {
        super(name, height, weight, color);
    }

    // Overriding the makeSound method for a Dog
    @Override
    public void makeSound() {
        System.out.println(getName() + " says Woof! Woof!");
    }
}

// AnimalShelter.java
// Main class to demonstrate the Animal hierarchy
public class AnimalShelter {
    public static void main(String[] args) {
        // Creating instances of different animals
        Cow bessy = new Cow("Bessy", 1.5, 800, "White and Black");
        Cat whiskers = new Cat("Whiskers", 0.3, 4, "Ginger");
        Dog buddy = new Dog("Buddy", 0.6, 25, "Golden");

        // Demonstrating inherited and overridden methods
        System.out.println("--- Animal Details ---");
        System.out.println(bessy);
        bessy.eat();
        bessy.sleep();
        bessy.makeSound();
        System.out.println();

        System.out.println(whiskers);
        whiskers.eat();
        whiskers.sleep();
        whiskers.makeSound();
        System.out.println();

        System.out.println(buddy);
        buddy.eat();
        buddy.sleep();
        buddy.makeSound();
        System.out.println();
    }
}
3.2. Shapes and Area/Perimeter Calculation (Lab 2, Example 1)

Implement classes for different shapes: Circle, Rectangle, Triangle, Square, Ellipse. Add corresponding members and methods to calculate the area and perimeter of the shapes. Use inheritance for minimizing amount of code.

Click to see the solution
import static java.lang.Math.PI; // Import PI for calculations
import static java.lang.Math.sqrt; // Import sqrt for calculations

// Shape.java
// The abstract Shape class defines common methods for all shapes.
abstract class Shape {
    // All shapes will have a name, though not explicitly asked, it's good practice.
    private String name;

    public Shape(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    // Abstract methods for calculating area and perimeter,
    // to be implemented by concrete shape subclasses.
    public abstract double getArea();
    public abstract double getPerimeter();

    @Override
    public String toString() {
        return "Shape: " + name;
    }
}

// Circle.java
// Circle extends Shape and implements area and perimeter calculation for a circle.
class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        super("Circle"); // Set the name of the shape
        if (radius <= 0) {
            throw new IllegalArgumentException("Radius must be positive.");
        }
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    // Area of a circle: PI * r^2
    @Override
    public double getArea() {
        return PI * radius * radius;
    }

    // Perimeter (circumference) of a circle: 2 * PI * r
    @Override
    public double getPerimeter() {
        return 2 * PI * radius;
    }

    @Override
    public String toString() {
        return super.toString() + ", Radius: " + radius;
    }
}

// Rectangle.java
// Rectangle extends Shape and implements area and perimeter calculation for a rectangle.
class Rectangle extends Shape {
    private double length;
    private double width;

    public Rectangle(double length, double width) {
        super("Rectangle"); // Set the name of the shape
        if (length <= 0 || width <= 0) {
            throw new IllegalArgumentException("Length and width must be positive.");
        }
        this.length = length;
        this.width = width;
    }

    public double getLength() {
        return length;
    }

    public double getWidth() {
        return width;
    }

    // Area of a rectangle: length * width
    @Override
    public double getArea() {
        return length * width;
    }

    // Perimeter of a rectangle: 2 * (length + width)
    @Override
    public double getPerimeter() {
        return 2 * (length + width);
    }

    @Override
    public String toString() {
        return super.toString() + ", Length: " + length + ", Width: " + width;
    }
}

// Square.java
// Square extends Rectangle, demonstrating inheritance for specialized shapes.
class Square extends Rectangle {
    public Square(double side) {
        // Call the Rectangle constructor with length and width being the same (side)
        super(side, side);
        super.name = "Square"; // Override the name from "Rectangle" to "Square"
    }

    // No need to override getArea() or getPerimeter() as Rectangle's methods work correctly.
    // However, we can add a specific toString for Square.
    @Override
    public String toString() {
        // Using getLength() from the parent Rectangle class which is the side.
        return super.toString().replace("Length: " + getLength() + ", Width: " + getLength(), "Side: " + getLength());
    }
}

// Triangle.java
// Triangle extends Shape and implements area and perimeter calculation for a triangle.
// For simplicity, area uses base and height, and perimeter uses three sides.
class Triangle extends Shape {
    private double sideA;
    private double sideB;
    private double sideC;
    private double base; // For area calculation
    private double height; // For area calculation

    // Constructor for perimeter (given three sides)
    public Triangle(double sideA, double sideB, double sideC) {
        super("Triangle");
        // Basic check for valid triangle (triangle inequality theorem)
        if (sideA <= 0 || sideB <= 0 || sideC <= 0 ||
            (sideA + sideB <= sideC) || (sideA + sideC <= sideB) || (sideB + sideC <= sideA)) {
            throw new IllegalArgumentException("Invalid triangle sides.");
        }
        this.sideA = sideA;
        this.sideB = sideB;
        this.sideC = sideC;
        // For area, we might need base and height or use Heron's formula if only sides are given.
        // Let's assume for this constructor, area calculation would need additional info or Heron's.
        // For simplicity, we'll make a second constructor for base/height for area,
        // or a method to set base/height if they are not explicitly part of the constructor.
    }

    // Constructor for area (given base and height) and perimeter (given three sides)
    public Triangle(double base, double height, double sideA, double sideB, double sideC) {
        this(sideA, sideB, sideC); // Call the other constructor to validate sides
        if (base <= 0 || height <= 0) {
            throw new IllegalArgumentException("Base and height must be positive.");
        }
        this.base = base;
        this.height = height;
    }

    // Area of a triangle: 0.5 * base * height
    // If only sides are known, Heron's formula would be used.
    @Override
    public double getArea() {
        if (base > 0 && height > 0) {
            return 0.5 * base * height;
        } else {
            // Using Heron's formula if base/height not provided, assuming sides are valid.
            double s = (sideA + sideB + sideC) / 2; // semi-perimeter
            return sqrt(s * (s - sideA) * (s - sideB) * (s - sideC));
        }
    }

    // Perimeter of a triangle: sideA + sideB + sideC
    @Override
    public double getPerimeter() {
        return sideA + sideB + sideC;
    }

    @Override
    public String toString() {
        return super.toString() + ", Sides: " + sideA + ", " + sideB + ", " + sideC +
               (base > 0 && height > 0 ? ", Base: " + base + ", Height: " + height : "");
    }
}

// Ellipse.java
// Ellipse extends Shape and implements area and perimeter calculation for an ellipse.
// Perimeter of an ellipse has no simple exact formula, using Ramanujan's approximation.
class Ellipse extends Shape {
    private double semiMajorAxis; // 'a'
    private double semiMinorAxis; // 'b'

    public Ellipse(double semiMajorAxis, double semiMinorAxis) {
        super("Ellipse");
        if (semiMajorAxis <= 0 || semiMinorAxis <= 0) {
            throw new IllegalArgumentException("Semi-axes must be positive.");
        }
        this.semiMajorAxis = Math.max(semiMajorAxis, semiMinorAxis); // Ensure a >= b
        this.semiMinorAxis = Math.min(semiMajorAxis, semiMinorAxis);
    }

    public double getSemiMajorAxis() {
        return semiMajorAxis;
    }

    public double getSemiMinorAxis() {
        return semiMinorAxis;
    }

    // Area of an ellipse: PI * a * b
    @Override
    public double getArea() {
        return PI * semiMajorAxis * semiMinorAxis;
    }

    // Perimeter of an ellipse (Ramanujan's second approximation):
    // PI * [3*(a+b) - sqrt((3a+b)*(a+3b))]
    @Override
    public double getPerimeter() {
        double a = semiMajorAxis;
        double b = semiMinorAxis;
        return PI * (3 * (a + b) - sqrt((3 * a + b) * (a + 3 * b)));
    }

    @Override
    public String toString() {
        return super.toString() + ", Semi-major Axis: " + semiMajorAxis + ", Semi-minor Axis: " + semiMinorAxis;
    }
}

// ShapeCalculator.java
// Main class to demonstrate the Shape hierarchy and calculations.
public class ShapeCalculator {
    public static void main(String[] args) {
        // Create an array of Shape objects
        Shape[] shapes = new Shape[5];

        shapes[0] = new Circle(5.0);
        shapes[1] = new Rectangle(4.0, 6.0);
        shapes[2] = new Square(5.0); // A square is a type of rectangle
        shapes[3] = new Triangle(3.0, 4.0, 5.0); // Right-angled triangle (sides)
        shapes[4] = new Ellipse(7.0, 4.0);

        System.out.println("--- Shape Calculations ---");
        for (Shape shape : shapes) {
            System.out.println(shape);
            System.out.printf("  Area: %.2f\n", shape.getArea());
            System.out.printf("  Perimeter: %.2f\n", shape.getPerimeter());
            System.out.println();
        }

        // Demonstrating a triangle with base and height
        Triangle triangleWithBaseHeight = new Triangle(6.0, 4.0, 5.0, 6.0, 7.0);
        System.out.println(triangleWithBaseHeight);
        System.out.printf("  Area (using base/height): %.2f\n", triangleWithBaseHeight.getArea());
        System.out.printf("  Perimeter: %.2f\n", triangleWithBaseHeight.getPerimeter());
        System.out.println();

        // Example of invalid input
        try {
            new Circle(-2.0);
        } catch (IllegalArgumentException e) {
            System.out.println("Error creating Circle: " + e.getMessage());
        }
        try {
            new Triangle(1.0, 2.0, 10.0); // Invalid triangle sides
        } catch (IllegalArgumentException e) {
            System.out.println("Error creating Triangle: " + e.getMessage());
        }
    }
}